using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Xml;
using System.Xml.Xsl;
using ABC.XsltEngine;
using Mvp.Xml.Common.Xsl;

namespace ABC.Mvc
{
	public class XsltViewEngine : VirtualPathProviderViewEngine
	{
		private static readonly string[] _emptyLocations = new string[0];
		private Func<string, string> GetExtensionThunk = VirtualPathUtility.GetExtension;
		private static MvpXslTransform _copyXmlTransform;
		private static readonly object Locker = new object();

		public MvpXslTransform CopyXmlTransform
		{
			get
			{
				if( _copyXmlTransform == null )
					lock( Locker )
						if( _copyXmlTransform == null )
						{
							var doc = new XmlDocument();
							doc.LoadXml( @"	<xsl:stylesheet version=""1.0"" xmlns:xsl=""http://www.w3.org/1999/XSL/Transform""
												xmlns:sdf=""http://www.abc.com/schemas/ABC""
												xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"">

												<xsl:template match=""*"">
													<xsl:param name=""depth"" select=""0""/>

													<xsl:if test=""$depth &lt; 6"">
														<xsl:copy>
															<xsl:apply-templates select=""@* | node()"">
																<xsl:with-param name=""depth"" select=""$depth + 1""/>
															</xsl:apply-templates>
														</xsl:copy>
													</xsl:if>
												</xsl:template>

												<xsl:template match=""@*"">
													<xsl:copy>
														<xsl:value-of select="".""/>
													</xsl:copy>
												</xsl:template>
											</xsl:stylesheet>" );

							_copyXmlTransform = new MvpXslTransform();
							_copyXmlTransform.Load( doc );
						}
				return _copyXmlTransform;
			}
		}

		public XsltViewEngine()
		{
			AreaViewLocationFormats = new[]
			                          	{
			                          		"~/Areas/{2}/Views/{1}/{0}.xsl", "~/Areas/{2}/Views/{1}/{0}.xslt",
			                          		"~/Areas/{2}/Views/Shared/{0}.xsl", "~/Areas/{2}/Views/Shared/{0}.xslt"
			                          	};
			AreaMasterLocationFormats = new[]
			                            	{
			                            		"~/Areas/{2}/Views/{1}/{0}.xsl", "~/Areas/{2}/Views/{1}/{0}.xslt",
			                            		"~/Areas/{2}/Views/Shared/{0}.xsl", "~/Areas/{2}/Views/Shared/{0}.xslt"
			                            	};
			AreaPartialViewLocationFormats = new[]
			                                 	{
			                                 		"~/Areas/{2}/Views/{1}/{0}.xsl", "~/Areas/{2}/Views/{1}/{0}.xslt",
			                                 		"~/Areas/{2}/Views/Shared/{0}.xsl", "~/Areas/{2}/Views/Shared/{0}.xslt"
			                                 	};
			ViewLocationFormats = new[]
			                      	{
			                      		"~/Views/{1}/{0}.xsl", "~/Views/{1}/{0}.xslt", "~/Views/Shared/{0}.xsl",
			                      		"~/Views/Shared/{0}.xslt"
			                      	};
			MasterLocationFormats = new[]
			                        	{
			                        		"~/Views/{1}/{0}.xsl", "~/Views/{1}/{0}.xslt", "~/Views/Shared/{0}.xsl",
			                        		"~/Views/Shared/{0}.xslt"
			                        	};
			PartialViewLocationFormats = new[]
			                             	{
			                             		"~/Views/{1}/{0}.xsl", "~/Views/{1}/{0}.xslt", "~/Views/Shared/{0}.xsl",
			                             		"~/Views/Shared/{0}.xslt"
			                             	};
			FileExtensions = new[] { "xsl", "xslt" };
		}

		protected override IView CreatePartialView( ControllerContext controllerContext, string partialPath )
		{
			return CreateView( controllerContext, partialPath, null );
		}

		protected override IView CreateView( ControllerContext controllerContext, string viewPath, string masterPath )
		{
			// Load transform
			var transform = new MvpXslTransform();
			var resolver = new MvcXmlResolver( VirtualPathProvider, viewPath );

			if (viewPath.Contains("theme") && !string.IsNullOrEmpty(HttpContext.Current.Theme()))
				viewPath = viewPath.Replace("theme", HttpContext.Current.Theme());
			if( "false".Equals( controllerContext.HttpContext.Request.Params["transform"], StringComparison.OrdinalIgnoreCase ) )
			{
				if( controllerContext.HttpContext.Response.ContentType.Equals( "text/html", StringComparison.OrdinalIgnoreCase ) )
					controllerContext.HttpContext.Response.ContentType = "text/xml";
				return new XsltView( CopyXmlTransform, null );
			}

			using( var viewStream = VirtualPathProvider.GetFile( viewPath ).Open() )
			using( var xmlReader = XmlReader.Create( viewStream, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore } ) )
				transform.Load( xmlReader, XsltSettings.TrustedXslt, resolver );

			return new XsltView( transform, resolver );
		}

		public override ViewEngineResult FindPartialView( ControllerContext controllerContext, string partialViewName, bool useCache )
		{
			string[] viewLocations;
			if( controllerContext == null )
				throw new ArgumentNullException( "controllerContext" );
			if( string.IsNullOrEmpty( partialViewName ) )
				throw new ArgumentException( "Value cannot be null or empty.", "partialViewName" );
			string controllerName = controllerContext.RouteData.GetRequiredString( "controller" );
			string partialPath = GetPath( controllerContext, PartialViewLocationFormats, AreaPartialViewLocationFormats, "PartialViewLocationFormats", partialViewName, controllerName, HttpContext.Current.Theme(), "Partial", useCache, out viewLocations );
			if( string.IsNullOrEmpty( partialPath ) )
				return new ViewEngineResult( viewLocations );
			return new ViewEngineResult( CreatePartialView( controllerContext, partialPath ), this );
		}

		public override ViewEngineResult FindView( ControllerContext controllerContext, string viewName, string masterName, bool useCache )
		{
			string[] viewLocations;
			string[] areaLocations;

			if( controllerContext == null )
				throw new ArgumentNullException( "controllerContext" );
			if( string.IsNullOrEmpty( viewName ) )
				throw new ArgumentException( "Value cannot be null or empty.", "viewName" );
			string controllerName = controllerContext.RouteData.GetRequiredString( "controller" );
			string viewPath = GetPath(controllerContext, ViewLocationFormats, AreaViewLocationFormats, "ViewLocationFormats", viewName, controllerName, HttpContext.Current.Theme(), "View", useCache, out viewLocations);
			string areaPath = GetPath( controllerContext, MasterLocationFormats, AreaMasterLocationFormats, "MasterLocationFormats", masterName, controllerName, HttpContext.Current.Theme(), "Master", useCache, out areaLocations );
			if( !string.IsNullOrEmpty( viewPath ) && ( !string.IsNullOrEmpty( areaPath ) || string.IsNullOrEmpty( masterName ) ) )
				return new ViewEngineResult( CreateView( controllerContext, viewPath, areaPath ), this );
			return new ViewEngineResult( viewLocations.Union( areaLocations ) );
		}

		private string CreateCacheKey( string prefix, string name, string controllerName, string areaName )
		{
			return string.Format( CultureInfo.InvariantCulture, ":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:", new object[] { base.GetType().AssemblyQualifiedName, prefix, name, controllerName, areaName } );
		}

		private bool FilePathIsSupported( string virtualPath )
		{
			if( FileExtensions == null )
				return true;
			string str = GetExtensionThunk( virtualPath ).TrimStart( new[] { '.' } );
			return FileExtensions.Contains( str, StringComparer.OrdinalIgnoreCase );
		}

		private string GetPath( ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string theme, string cacheKeyPrefix, bool useCache, out string[] searchedLocations )
		{
			searchedLocations = _emptyLocations;
			if( string.IsNullOrEmpty( name ) )
				return string.Empty;
			string areaName = AreaHelpers.GetAreaName( controllerContext.RouteData );
			bool flag = !string.IsNullOrEmpty( areaName );
			var viewLocations = GetViewLocations( locations, flag ? areaLocations : null );
			if( viewLocations.Count == 0 )
				throw new InvalidOperationException( string.Format( CultureInfo.CurrentCulture, "The property '{0}' cannot be null or empty.", new object[] { locationsPropertyName } ) );
			bool flag2 = IsSpecificPath( name );
			string key = CreateCacheKey( cacheKeyPrefix, name, flag2 ? string.Empty : controllerName, areaName );
			if( useCache )
				return ViewLocationCache.GetViewLocation( controllerContext.HttpContext, key );
			if( !flag2 )
				return GetPathFromGeneralName( controllerContext, viewLocations, name, controllerName, areaName, theme, key, ref searchedLocations );
			return GetPathFromSpecificName( controllerContext, name, key, ref searchedLocations );
		}

		private string GetPathFromGeneralName( ControllerContext controllerContext, List<ViewLocation> locations, string name, string controllerName, string areaName, string theme, string cacheKey, ref string[] searchedLocations )
		{
			string virtualPath = string.Empty;
			searchedLocations = new string[locations.Count];
			for( int i = 0; i < locations.Count; i++ )
			{
				string location = locations[i].Format( name, controllerName, areaName, theme );
				if( FileExists( controllerContext, location ) )
				{
					searchedLocations = _emptyLocations;
					virtualPath = location;
					ViewLocationCache.InsertViewLocation( controllerContext.HttpContext, cacheKey, virtualPath );
					return virtualPath;
				}
				searchedLocations[i] = location;
			}
			return virtualPath;
		}

		private string GetPathFromSpecificName( ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations )
		{
			string virtualPath = name;
			if( !FilePathIsSupported( name ) || !FileExists( controllerContext, name ) )
			{
				virtualPath = string.Empty;
				searchedLocations = new[] { name };
			}
			ViewLocationCache.InsertViewLocation( controllerContext.HttpContext, cacheKey, virtualPath );
			return virtualPath;
		}

		private static List<ViewLocation> GetViewLocations( string[] viewLocationFormats, string[] areaViewLocationFormats )
		{
			var list = new List<ViewLocation>();
			if( areaViewLocationFormats != null )
				list.AddRange( areaViewLocationFormats.Select( str => new AreaAwareViewLocation( str ) ) );
			if( viewLocationFormats != null )
				list.AddRange( viewLocationFormats.Select( str2 => new ViewLocation( str2 ) ) );
			return list;
		}

		private static bool IsSpecificPath( string name )
		{
			char ch = name[0];
			if( ch != '~' )
				return ( ch == '/' );
			return true;
		}

		private class ViewLocation
		{
			protected string _virtualPathFormatString;

			public ViewLocation( string virtualPathFormatString )
			{
				_virtualPathFormatString = virtualPathFormatString;
			}

			public virtual string Format( string viewName, string controllerName, string areaName, string theme )
			{
				return string.Format( CultureInfo.InvariantCulture, _virtualPathFormatString, new object[] { viewName, controllerName, theme } );
			}
		}

		private class AreaAwareViewLocation : ViewLocation
		{
			// Methods
			public AreaAwareViewLocation( string virtualPathFormatString )
				: base( virtualPathFormatString )
			{
			}

			public override string Format( string viewName, string controllerName, string areaName, string theme )
			{
				return string.Format( CultureInfo.InvariantCulture, _virtualPathFormatString, new object[] { viewName, controllerName, areaName, theme } );
			}
		}
	}

	internal static class AreaHelpers
	{
		public static string GetAreaName( RouteBase routeBase )
		{
			var area = routeBase as IRouteWithArea;
			if( area != null )
				return area.Area;
			var route = routeBase as Route;
			if( ( route != null ) && ( route.DataTokens != null ) )
				return ( route.DataTokens["area"] as string );
			return null;
		}

		public static string GetAreaName( RouteData routeData )
		{
			object area;
			if( routeData.DataTokens.TryGetValue( "area", out area ) )
				return ( area as string );
			return GetAreaName( routeData.Route );
		}
	}
}